package com.web.music.impl;

import java.io.File;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import com.web.music.common.MusicException;
import com.web.music.model.*;
import com.web.music.xml.MusicXmlErrorHandler;
import com.web.music.xml.XmlSchemaConst;
import org.apache.log4j.*;

/**
 * This implementation uses JAXP API in combination with DOM API.
 * 
 * TODO [done] 
 * Your task is to extend XmlParser interface to extract data from catalog.xml.
 * You can decide to extend this implementation or use another approach/framework for XML parsing.
 * Anyway, do not forget to write unit-tests for new functionality!
 */
public class DomXmlParser extends BaseXmlParser {

	static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
	static Logger logger = Logger.getLogger(DomXmlParser.class.getName());



	/**
	 * This method is called from LocalMusicProcessor.
	 * 
	 * Obtain DOM Document by parsing file received as an argument.
	 * Continue with DOM parsing.
	 * 
	 * @return a map <person id, person object>
	 */
	public Map<String,Person> extractPeopleInfo(File xmlFile){
		logger.info("[DomXmlParser] Starting to extract people info from File");
		Document doc = null;
		try {			
			doc = getDocumentBuilder().parse(xmlFile);
		} catch (Exception e) {
			throw new MusicException(e);
		}
		logger.info("[DomXmlParser] DOM Document successfully created");
		return extractPeopleInfo(doc);
	}

	/**
	 * This method is called from RemoteMusicProcessor.
	 * 
	 * Obtain DOM Document by parsing input stream received as an argument.
	 * Continue with DOM parsing.
	 * 
	 * @return a map <person id, person object>
	 */
	public Map<String,Person> extractPeopleInfo(InputStream input){
		logger.info("[DomXmlParser] Starting to extract people info from InputStream");
		Document doc = null;
		try {
			doc = getDocumentBuilder().parse(input);
		} catch (Exception e) {
			throw new MusicException(e);
		}
		logger.info("[DomXmlParser] DOM Document successfully created");
		return extractPeopleInfo(doc);
	}	

	/**
	 * Initialization of DocumentBuilder to be used to obtain DOM Document.
	 */
	private DocumentBuilder getDocumentBuilder(){
		try {
			DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
			factory.setNamespaceAware(true);
			// specifies if XML document will be validated against XML Schema (XSD file)
			// if document will not correspond to schema definition, then there will be an error
			factory.setValidating(isValidateXML());
			try {
				factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
			} 
			catch (IllegalArgumentException e) {
				throw new MusicException(e);
			} 
			DocumentBuilder db = factory.newDocumentBuilder();
			// define error handle class to report XML processing errors
			db.setErrorHandler(new MusicXmlErrorHandler());
			return db;
		} catch (ParserConfigurationException e) {
			throw new MusicException(e);
		}

	}

	/**
	 * Parse DOM document and extract people information from it.
	 * 
	 * @param doc
	 * @return a map <person id, person object>
	 */
	private Map<String,Person> extractPeopleInfo(Document doc)
	{
		Map<String,Person> people = new HashMap<String,Person>();

		// go through the Document and extract people info
		Element root = doc.getDocumentElement();
		for (Node personNode = root.getFirstChild(); personNode != null; personNode = personNode.getNextSibling())
		{	
			if (!XmlSchemaConst.Entities.PERSON.equals(personNode.getNodeName())){
//				System.out.println("Skipping node: " + personNode);
				continue;
			}
			NamedNodeMap attrMap = personNode.getAttributes();
			if (attrMap == null){
				logger.warn("Node doesn't have attributes: " + personNode);
				continue;
			}

			Person person = new Person();
			Node idNode = attrMap.getNamedItem(XmlSchemaConst.Person.ID);
			if (idNode == null){
				logger.warn("Person node attribute 'id' is null: " + personNode);
				continue;
			}

			String personIdString = idNode.getNodeValue();
			person.setId(extractIdValue(personIdString, XmlSchemaConst.IdPrefixes.PERSON_ID_PREFIX));

			// read remaining person attributes
			NodeList personNodes = personNode.getChildNodes();
			for (int i=0; i<personNodes.getLength(); i++){
				Node node = personNodes.item(i);
				if (XmlSchemaConst.Person.NAME.equals(node.getNodeName())){
					person.setName(node.getTextContent());	
					continue;
				}
				if (XmlSchemaConst.Person.SURNAME.equals(node.getNodeName())){
					person.setSurname(node.getTextContent());
					continue;
				}
			}
			people.put(personIdString,person);
		}
		logger.info("[DomXmlParser] People info successfully extracted. In total [" + people.size() + "] items extracted");
		return people;
	}

	public Map<String,Genre> extractGenreInfo(File xmlFile){
		logger.info("[DomXmlParser] Starting to extract genre info from File");
		Document doc = null;
		try {			
			doc = getDocumentBuilder().parse(xmlFile);
		} catch (Exception e) {
			throw new MusicException(e);
		}
		logger.info("[DomXmlParser] DOM Document successfully created");
		return extractGenreInfo(doc);
	}

	public Map<String,Genre> extractGenreInfo(InputStream input){
		logger.info("[DomXmlParser] Starting to extract Genre info from InputStream");
		Document doc = null;
		try {
			doc = getDocumentBuilder().parse(input);
		} catch (Exception e) {
			throw new MusicException(e);
		}
		logger.info("[DomXmlParser] DOM Document successfully created");
		return extractGenreInfo(doc);		
	}

	private Map<String,Genre> extractGenreInfo(Document doc){
		Map<String,Genre> genres = new HashMap<String,Genre>();

		Element root = (Element) doc.getElementsByTagName("genres").item(0);
		for (Node genreNode = root.getFirstChild(); genreNode != null; genreNode = genreNode.getNextSibling())
		{	
			if (!XmlSchemaConst.Entities.GENRE.equals(genreNode.getNodeName())){
//				System.out.println("Skipping node: " + genreNode);
				continue;
			}
			NamedNodeMap attrMap = genreNode.getAttributes();
			if (attrMap == null){
				logger.warn("Node doesn't have attributes: " + genreNode);
				continue;
			}

			Genre genre = new Genre();
			Node idNode = attrMap.getNamedItem(XmlSchemaConst.Genre.ID);
			if (idNode == null){
				logger.warn("Genre node attribute 'id' is null: " + genreNode);
				continue;
			}

			String genreIdString = idNode.getNodeValue();
			genre.setId(extractIdValue(genreIdString, XmlSchemaConst.IdPrefixes.GENRE_ID_PREFIX));

			// read remaining person attributes
			NodeList genreNodes = genreNode.getChildNodes();
			for (int i=0; i<genreNodes.getLength(); i++){
				Node node = genreNodes.item(i);
				if (XmlSchemaConst.Genre.NAME.equals(node.getNodeName())){
					genre.setName(node.getTextContent());	
					continue;
				}
			}
			genres.put(genreIdString,genre);
		}
		logger.info("[DomXmlParser] Genre info successfully extracted. In total [" + genres.size() + "] items extracted");

		return genres;
	}

	public Map<String,Artist> extractArtistInfo(File xmlFile,Map<String,Genre> genres ,Map<String,Person> people){
		logger.info("[DomXmlParser] Starting to extract artist info from File");
		Document doc = null;
		try {			
			doc = getDocumentBuilder().parse(xmlFile);
		} catch (Exception e) {
			throw new MusicException(e);
		}
		logger.info("[DomXmlParser] DOM Document successfully created");
		return extractArtistInfo(doc, genres, people);
	}

	public Map<String,Artist> extractArtistInfo(InputStream input, Map<String,Genre> genres ,Map<String,Person> people){
		logger.info("[DomXmlParser] Starting to extract Artist info from InputStream");
		Document doc = null;
		try {
			doc = getDocumentBuilder().parse(input);
		} catch (Exception e) {
			throw new MusicException(e);
		}
		logger.info("[DomXmlParser] DOM Document successfully created");
		return extractArtistInfo(doc, genres, people);		
	}	

	private Map<String, Artist> extractArtistInfo(Document doc, Map<String,Genre> genres ,Map<String,Person> people){

		//Map<String,Genre> genres = extractGenreInfo(doc);
		//Map<String,Person> people = extractPeopleInfo(doc);

		Map<String, Artist> artists = new HashMap<String, Artist>();

		Element root = (Element) doc.getElementsByTagName("artists").item(0);
		for (Node artistNode = root.getFirstChild(); artistNode != null; artistNode = artistNode.getNextSibling())
		{	
			if (!XmlSchemaConst.Entities.ARTIST.equals(artistNode.getNodeName())){
//				System.out.println("Skipping node: " + genreNode);
				continue;
			}
			NamedNodeMap attrMap = artistNode.getAttributes();
			if (attrMap == null){
				logger.warn("Node doesn't have attributes: " + artistNode);
				continue;
			}

			Node idNode = attrMap.getNamedItem(XmlSchemaConst.Artist.ID);
			if (idNode == null){
				logger.warn("Artist node attribute 'id' is null: " + artistNode);
				continue;
			}
			Node typeNode = attrMap.getNamedItem(XmlSchemaConst.Artist.TYPE);
			if (typeNode ==null){
				logger.warn("Artist node attribute 'type' is null: " + artistNode);
				continue;
			}

			String artistIdString = idNode.getNodeValue();
			String artistTypeString = typeNode.getNodeValue();
			Artist artist;
			List<Person> members = new ArrayList<Person>();
			List<Genre> artist_genres = new ArrayList<Genre>();
			Person tmp_member;
			Genre tmp_genre;
			
			if (artistTypeString.equals(XmlSchemaConst.Artist.TYPE_BAND)){
				MusicBand band = new MusicBand();
				band.setId(extractIdValue(artistIdString, XmlSchemaConst.IdPrefixes.ARTIST_ID_PREFIX));
				// read remaining Band attributes
				NodeList artistNodes = artistNode.getChildNodes();
				for (int i=0; i<artistNodes.getLength(); i++){
					Node node = artistNodes.item(i);
					if (XmlSchemaConst.Artist.TITLE.equals(node.getNodeName())){
						band.setTitle(node.getTextContent());	
						continue;
					}
					if (XmlSchemaConst.Artist.FOUNDATION_DATE.equals(node.getNodeName())){
						band.setFoundationDate(node.getTextContent());	
						continue;
					}		
					if (XmlSchemaConst.Artist.MEMBER.equals(node.getNodeName())){
						tmp_member = people.get(node.getTextContent());
						
						tmp_member.addMusicBand(band);
						
						members.add(tmp_member);
					}
					if (XmlSchemaConst.Artist.GENRE.equals(node.getNodeName())){
						NamedNodeMap gattrMap = node.getAttributes();
						Node gNode = gattrMap.getNamedItem(XmlSchemaConst.IDREF);
						if (gNode==null){
							logger.warn("Genre node attribute 'type' is null: " + gNode);
							continue;							
						}

						tmp_genre = genres.get(gNode.getNodeValue());
						artist_genres.add(tmp_genre);
					}
				}	
				band.setMembers(members);

				artist = band;
			}
			else{
				Singer singer = new Singer();	
				singer.setId(extractIdValue(artistIdString, XmlSchemaConst.IdPrefixes.ARTIST_ID_PREFIX));				
				// read remaining Singer attributes
				NodeList artistNodes = artistNode.getChildNodes();
				for (int i=0; i<artistNodes.getLength(); i++){
					Node node = artistNodes.item(i);
					if (XmlSchemaConst.Artist.NAME.equals(node.getNodeName())){
						singer.setName(node.getTextContent());	
						continue;
					}
					if (XmlSchemaConst.Artist.CITY_OF_ORIGIN.equals(node.getNodeName())){
						singer.setCityOfOrigin(node.getTextContent());	
						continue;
					}	
					if (XmlSchemaConst.Artist.PERSON.equals(node.getNodeName())){
						tmp_member = people.get(node.getTextContent());
						singer.setPerson(tmp_member);
						tmp_member.setSingerInfo(singer);
					}
					if (XmlSchemaConst.Artist.GENRE.equals(node.getNodeName())){
						NamedNodeMap gattrMap = node.getAttributes();
						Node gNode = gattrMap.getNamedItem(XmlSchemaConst.IDREF);
						if (gNode==null){
							logger.warn("Genre node attribute 'type' is null: " + gNode);
							continue;							
						}

						tmp_genre = genres.get(gNode.getNodeValue());
						artist_genres.add(tmp_genre);
					}					
				}		
				artist = singer;
			}
			artist.setGenres(artist_genres);					
			
			artists.put(artistIdString,artist);
		}
		logger.info("[DomXmlParser] Artist info successfully extracted. In total [" + artists.size() + "] items extracted");		
		return artists;
	}

	public Map<String, Album> extractAlbumInfo(File xmlFile, Map<String, Artist> artists,Map<String,Genre> genres){
		logger.info("[DomXmlParser] Starting to extract Album info from InputStream");
		Document doc = null;
		try {
			doc = getDocumentBuilder().parse(xmlFile);
		} catch (Exception e) {
			throw new MusicException(e);
		}
		logger.info("[DomXmlParser] DOM Document successfully created");
		return extractAlbumInfo(doc, artists, genres);		
	}

	public Map<String, Album> extractAlbumInfo(InputStream input, Map<String, Artist> artists,Map<String,Genre> genres){
		logger.info("[DomXmlParser] Starting to extract Album info from InputStream");
		Document doc = null;
		try {
			doc = getDocumentBuilder().parse(input);
		} catch (Exception e) {
			throw new MusicException(e);
		}
		logger.info("[DomXmlParser] DOM Document successfully created");
		return extractAlbumInfo(doc, artists, genres);		
	}
	private Map<String, Album> extractAlbumInfo(Document doc, Map<String, Artist> artists, Map<String,Genre> genres){

		Map<String, Album> albums = new HashMap<String, Album>();

		Element root = (Element) doc.getElementsByTagName("albums").item(0);
		for (Node albumNode = root.getFirstChild(); albumNode != null; albumNode = albumNode.getNextSibling())
		{		

			if (!XmlSchemaConst.Entities.ALBUM.equals(albumNode.getNodeName())){
//				System.out.println("Skipping node: " + genreNode);
				continue;
			}

			NamedNodeMap attrMap = albumNode.getAttributes();
			if (attrMap == null){
				logger.warn("Node doesn't have attributes: " + albumNode);
				continue;
			}

			Album album = new Album();
			Genre tmp_genre;
			Artist album_artist=null;// = new Artist();
			List<Genre> album_genres = new ArrayList<Genre>();
			Track tmp_track;
			//List<Track> album_tracks = new ArrayList<Track>();
			
			Node idNode = attrMap.getNamedItem(XmlSchemaConst.Album.ID);
			if (idNode == null){
				logger.warn("Artist node attribute 'id' is null: " + albumNode);
				continue;
			}
			String albumIdString = idNode.getNodeValue();
			album.setId(extractIdValue(albumIdString, XmlSchemaConst.IdPrefixes.ALBUM_ID_PREFIX));

			// read remaining Album attributes
			NodeList albumNodes = albumNode.getChildNodes();
			for (int i=0; i<albumNodes.getLength(); i++){
				Node node = albumNodes.item(i);
				if (XmlSchemaConst.Album.TITLE.equals(node.getNodeName())){
					album.setTitle(node.getTextContent());	
					continue;
				}			
				if (XmlSchemaConst.Album.YEAR.equals(node.getNodeName())){
					album.setYear(node.getTextContent());	
					continue;
				}
				if (XmlSchemaConst.Album.GENRE.equals(node.getNodeName())){
					NamedNodeMap gattrMap = node.getAttributes();
					Node gNode = gattrMap.getNamedItem(XmlSchemaConst.IDREF);
					if (gNode==null){
						logger.warn("Genre node attribute 'id' is null: " + gNode);
						continue;							
					}

					tmp_genre = genres.get(gNode.getNodeValue());
					tmp_genre.getAlbums().add(album);
					album_genres.add(tmp_genre);
				}	

				if (XmlSchemaConst.Album.ARTIST.equals(node.getNodeName())){
					NamedNodeMap ArtattrMap = node.getAttributes();
					Node ArtNode = ArtattrMap.getNamedItem(XmlSchemaConst.IDREF);
					if (ArtNode==null){
						logger.warn("Artist node attribute 'id' is null: " + ArtNode);
						continue;							
					}
					album_artist = artists.get(ArtNode.getNodeValue());
				}

				if (XmlSchemaConst.Album.TRACK.equals(node.getNodeName())){
					NamedNodeMap gattrMap = node.getAttributes();
					Node gNode = gattrMap.getNamedItem(XmlSchemaConst.Track.NUM);

					tmp_track = new Track();

					if (gNode==null){
						logger.warn("Track node attribute 'num' is null: " + gNode);
						continue;							
					}
					int num = Integer.parseInt(gNode.getNodeValue());
					tmp_track.setNum(num);

					gNode = gattrMap.getNamedItem(XmlSchemaConst.Track.TITLE);
					if (gNode==null){
						logger.warn("Track node attribute 'title' is null: " + gNode);
						continue;							
					}
					String title = gNode.getNodeValue();
					tmp_track.setTitle(title);

					gNode = gattrMap.getNamedItem(XmlSchemaConst.Track.MP3);
					if (gNode==null){
						logger.warn("Track node attribute 'mp3' is null: " + gNode);
						continue;							
					}
					String mp3 = gNode.getNodeValue();
					tmp_track.setMP3(mp3);
					tmp_track.setAlbum(album);

					//album_tracks.add(tmp_track);
					album.addTrack(tmp_track);

				}
			}
			logger.debug("Album ["+album.getTitle()+"] track count ["+album.getTracks().size()+"]");
			album.setGenres(album_genres);
			
			album.setArtist(album_artist);
			//album_artist.addAlbum(album); Avoid bidirectional relations, because of duplicated auto-gen records.
			
			albums.put(albumIdString, album);
		}
		return albums;

	}	

}